<!DOCTYPE HTML>
<html lang="ch" class="sidebar-visible no-js light">
    <head>
        <!-- Book generated using mdBook -->
        <meta charset="UTF-8">
        <title>nudt os lab book (2023)</title>
        <meta name="robots" content="noindex" />


        <!-- Custom HTML head -->
        
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="theme-color" content="#ffffff" />

        <link rel="icon" href="favicon.svg">
        <link rel="shortcut icon" href="favicon.png">
        <link rel="stylesheet" href="css/variables.css">
        <link rel="stylesheet" href="css/general.css">
        <link rel="stylesheet" href="css/chrome.css">
        <link rel="stylesheet" href="css/print.css" media="print">

        <!-- Fonts -->
        <link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
        <link rel="stylesheet" href="fonts/fonts.css">

        <!-- Highlight.js Stylesheets -->
        <link rel="stylesheet" href="highlight.css">
        <link rel="stylesheet" href="tomorrow-night.css">
        <link rel="stylesheet" href="ayu-highlight.css">

        <!-- Custom theme stylesheets -->

    </head>
    <body>
    <div id="body-container">
        <!-- Provide site root to javascript -->
        <script>
            var path_to_root = "";
            var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
        </script>

        <!-- Work around some values being stored in localStorage wrapped in quotes -->
        <script>
            try {
                var theme = localStorage.getItem('mdbook-theme');
                var sidebar = localStorage.getItem('mdbook-sidebar');

                if (theme.startsWith('"') && theme.endsWith('"')) {
                    localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
                }

                if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
                    localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
                }
            } catch (e) { }
        </script>

        <!-- Set the theme before any content is loaded, prevents flash -->
        <script>
            var theme;
            try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
            if (theme === null || theme === undefined) { theme = default_theme; }
            var html = document.querySelector('html');
            html.classList.remove('no-js')
            html.classList.remove('light')
            html.classList.add(theme);
            html.classList.add('js');
        </script>

        <!-- Hide / unhide sidebar before it is displayed -->
        <script>
            var html = document.querySelector('html');
            var sidebar = null;
            if (document.body.clientWidth >= 1080) {
                try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
                sidebar = sidebar || 'visible';
            } else {
                sidebar = 'hidden';
            }
            html.classList.remove('sidebar-visible');
            html.classList.add("sidebar-" + sidebar);
        </script>

        <nav id="sidebar" class="sidebar" aria-label="Table of contents">
            <div class="sidebar-scrollbox">
                <ol class="chapter"><li class="chapter-item expanded affix "><li class="part-title">操作系统实验指南</li><li class="chapter-item expanded "><a href="lab_0.html"><strong aria-hidden="true">1.</strong> 准备：实验环境搭建</a></li><li class="chapter-item expanded "><a href="lab_1.html"><strong aria-hidden="true">2.</strong> 实验1：向Framebuffer中打印字符</a></li><li class="chapter-item expanded "><a href="lab_2.html"><strong aria-hidden="true">3.</strong> 实验2：异常处理和硬件中断</a></li><li class="chapter-item expanded "><a href="lab_3.html"><strong aria-hidden="true">4.</strong> 实验3：内存管理</a></li><li class="chapter-item expanded "><a href="lab_4.html"><strong aria-hidden="true">5.</strong> 实验4：协程与异步</a></li></ol>
            </div>
            <div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
        </nav>

        <div id="page-wrapper" class="page-wrapper">

            <div class="page">
                                <div id="menu-bar-hover-placeholder"></div>
                <div id="menu-bar" class="menu-bar sticky bordered">
                    <div class="left-buttons">
                        <button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
                            <i class="fa fa-bars"></i>
                        </button>
                        <button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
                            <i class="fa fa-paint-brush"></i>
                        </button>
                        <ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
                            <li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
                        </ul>
                        <button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
                            <i class="fa fa-search"></i>
                        </button>
                    </div>

                    <h1 class="menu-title">nudt os lab book (2023)</h1>

                    <div class="right-buttons">
                        <a href="print.html" title="Print this book" aria-label="Print this book">
                            <i id="print-button" class="fa fa-print"></i>
                        </a>

                    </div>
                </div>

                <div id="search-wrapper" class="hidden">
                    <form id="searchbar-outer" class="searchbar-outer">
                        <input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
                    </form>
                    <div id="searchresults-outer" class="searchresults-outer hidden">
                        <div id="searchresults-header" class="searchresults-header"></div>
                        <ul id="searchresults">
                        </ul>
                    </div>
                </div>

                <!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
                <script>
                    document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
                    document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
                    Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
                        link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
                    });
                </script>

                <div id="content" class="content">
                    <main>
                        <h1 id="准备实验环境搭建"><a class="header" href="#准备实验环境搭建">准备：实验环境搭建</a></h1>
<h2 id="实验步骤"><a class="header" href="#实验步骤">实验步骤</a></h2>
<p>在连接互联网的情况下，可按如下步骤实施。</p>
<ol>
<li>
<p>安装Ubuntu2204虚拟机</p>
<ul>
<li><a href="https://ubuntu.com/download/desktop">Ubuntu2204下载</a></li>
<li><a href="https://www.vmware.com/in/products/workstation-player/workstation-player-evaluation.html">VmWare下载</a></li>
<li>创建新虚拟机，导入下载的Ubuntu2204镜像，按操作指引完成系统安装（磁盘容量大于50G）</li>
</ul>
</li>
<li>
<p>在虚拟机中安装Rust开发工具链</p>
<ul>
<li>安装依赖软件：
<ul>
<li><code>sudo apt install make gcc qemu-system-x86 curl</code></li>
</ul>
</li>
<li>按<a href="https://www.rust-lang.org/tools/install">官方说明</a>安装Rust</li>
<li>按以下命令安装工具
<ul>
<li><code>rustup target add x86_64-unknown-none</code></li>
<li><code>rustup component add llvm-tools-preview</code></li>
<li><code>rustup component add rust-src</code></li>
</ul>
</li>
</ul>
</li>
<li>
<p><a href="https://visualstudio.microsoft.com/zh-hans/">Visual Studio下载</a>及Rust工具安装</p>
<ul>
<li>打开VS，选择<code>扩展</code>按钮，搜索<code>rust-analyzer</code>并安装该插件</li>
<li>有关Rust的语法，可以阅读参考资料1.</li>
</ul>
</li>
<li>
<p>运行实验示例代码</p>
<ul>
<li>在VS中打开示例代码<code>rust_os-lab1</code>文件夹，并开启终端</li>
<li>在终端输入<code>make run</code>运行成功后应看到弹出的QEMU界面，如下所示：
<img src="pic/lab0-sc.png" alt="图片" /></li>
</ul>
</li>
</ol>
<p>至此，实验初始环境搭建完成。</p>
<p>大家可以阅读<code>README</code>文件，尝试运行更多的命令，并了解各命令的功能。</p>
<h2 id="实验示例代码说明"><a class="header" href="#实验示例代码说明">实验示例代码说明</a></h2>
<p>示例代码是一个最小可运行的系统，包括了boot和kernel两个主要模块，分别为加载器和内核。</p>
<p>由于内核本身无法独立运行，加载器的主要作用是将内核可执行程序打包为一个BIOS或UEFI规范可启动的镜像，系统加电后先执行加载器，之后加载器拉器内核。boot模块中的另一个作用是执行qemu命令，从而支持在模拟器中开发调试内核。目前，我们暂时先不关心加载器的运行原理，过多的细节容易入门即放弃。</p>
<p>我们的系统虽小，但已经有很多非常棒的功能辅助我们开发调试：</p>
<ul>
<li>
<p>串口输出。在主函数<code>kernel_main</code>中我们使用串口打印了<code>Hello world</code>，通过<code>make run</code>命令运行系统后，我们可以在终端看到输出。</p>
</li>
<li>
<p>GDB调试。代码中提供了一些小脚本可以方便的使用GDB进行调试，只需在一个终端运行<code>make gdb</code>，另一个终端运行<code>gdb -n -x .gdbinit</code>。GDB是Debug神器，如果不清楚如何使用，快查手册。</p>
</li>
<li>
<p>测试框架。代码中包括了单元测试和集成测试框架，kernel目录中包括了lib和main两个主文件，Rust会<a href="https://doc.rust-lang.org/cargo/guide/project-layout.html">分别</a>生成<code>libkernel库</code>和<code>kernel可执行程序</code>。lib实现了内核的主体逻辑，其中包含的#[test_case]函数为单元测试用例。main是内核的入口，到用了lib提供的功能。集成测试的用例类似于main，有独立的入口，复用lib提供的功能（如kernel/tests/chr2pixels.rs，可以有多个）。通过<code>make test</code>命令可以依次执行单元测试和各个集成测试。</p>
</li>
<li>
<p>注释文档。Rust支持<a href="https://doc.rust-lang.org/rustdoc/index.html">注释文档</a>机制，即通过特定的格式书写注释，便可自动生成文档。我们在各个主文件中标记了<code>#![warn(missing_docs)]</code>和<code>#![deny(warnings)]</code>，分别意为<code>缺少注释文档时warnings</code>和<code>拒绝一切warnings</code>，联合起来就是<code>不能缺少注释文档</code>，强迫大家写好注释，功在当代利在千秋。通过<code>make doc</code>命令可以查看自己写的注释文档。</p>
</li>
</ul>
<h2 id="参考资料"><a class="header" href="#参考资料">参考资料</a></h2>
<ol>
<li>Rust语言学习网站
<ul>
<li><a href="https://www.runoob.com/rust/rust-tutorial.html">https://www.runoob.com/rust/rust-tutorial.html</a></li>
<li><a href="https://course.rs/about-book.html">https://course.rs/about-book.html</a></li>
</ul>
</li>
<li>GDB调试工具使用: <a href="https://zhuanlan.zhihu.com/p/74897601">https://zhuanlan.zhihu.com/p/74897601</a></li>
<li>Rust语言实现操作系统博客
<ul>
<li>原版：<a href="https://os.phil-opp.com/">https://os.phil-opp.com/</a></li>
<li>中文翻译版：<a href="https://www.bookstack.cn/books/writing-an-os-in-rust">https://www.bookstack.cn/books/writing-an-os-in-rust</a></li>
</ul>
</li>
</ol>
<div style="break-before: page; page-break-before: always;"></div><h1 id="实验1向-framebuffer-中打印字符"><a class="header" href="#实验1向-framebuffer-中打印字符">实验1：向 Framebuffer 中打印字符</a></h1>
<h2 id="实验导读"><a class="header" href="#实验导读">实验导读</a></h2>
<p>大家已经完成了实验环境的搭建，并能够运行示例代码，显示系统启动时的信息。本次实验需要对系统功能进行扩展，提供向屏幕输出字符串的功能，以支持后续功能模块。</p>
<p>本实验提供一份基础代码，还有部分需要实验者自己补充，已在代码中用“TODO”标出。</p>
<ul>
<li><a href="lab_1.html#1-gdb%E8%B0%83%E8%AF%95%E7%BB%83%E4%B9%A0">1. GDB调试练习</a></li>
<li><a href="lab_1.html#2-%E7%86%9F%E6%82%89%E4%B8%B2%E5%8F%A3%E8%BE%93%E5%87%BA">2. 熟悉串口输出</a></li>
<li><a href="lab_1.html#3-%E6%98%BE%E5%8D%A1%E7%9A%84%E5%AD%97%E7%AC%A6%E6%A8%A1%E5%BC%8F%E4%B8%8E%E5%9B%BE%E5%BD%A2%E6%A8%A1%E5%BC%8F">3. 显卡的字符模式与图形模式</a></li>
<li><a href="lab_1.html#4-%E5%90%91-framebuffer-%E6%89%93%E5%8D%B0%E5%AD%97%E7%AC%A6">4. 向 Framebuffer 打印字符</a>
<ul>
<li><a href="lab_1.html#41-%E5%8C%85%E8%A3%85%E5%88%B0%E4%B8%80%E4%B8%AA-rust-%E6%A8%A1%E5%9D%97">4.1. 包装到一个 Rust 模块</a></li>
<li><a href="lab_1.html#42-framebufferdriver">4.2. FrameBufferDriver</a></li>
<li><a href="lab_1.html#43-framebufferdriver-%E7%9A%84%E7%A7%81%E6%9C%89%E5%87%BD%E6%95%B0">4.3. FrameBufferDriver 的私有函数</a></li>
<li><a href="lab_1.html#44-%E5%9C%A8%E5%B1%8F%E5%B9%95%E4%B8%8A%E8%BE%93%E5%87%BAhello-world%E5%AD%97%E7%AC%A6%E4%B8%B2">4.4. 在屏幕上输出“Hello world”字符串</a></li>
</ul>
</li>
<li><a href="lab_1.html#%E5%B0%8F%E7%BB%93">小结</a></li>
<li><a href="lab_1.html#%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%AE%9E%E9%AA%8C">下一个实验</a></li>
<li><a href="lab_1.html#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</a></li>
</ul>
<h2 id="1-gdb调试练习"><a class="header" href="#1-gdb调试练习">1. GDB调试练习</a></h2>
<p>QEMU可以结合GDB进行调试。使用 GDB 调试的原理是，QEMU
可以启动一个GDB远程目标（remote target）（使用-s或-S参数启动），QEMU会在真正执行镜像中的指令前等待GDB客户端的连接。开启远程目标之后，可以开启GDB进行调试，它会在某个端口上进行监听。</p>
<p>我们提供了一个GDB脚本<code>.gdbinit</code>来初始化GDB，并且设置了其监听端口为QEMU的默认端口（tcp::1234）。</p>
<pre><code>target remote :1234
symbol-file kernel/target/x86_64-unknown-none/debug/kernel -o 0x8000000000
b kernel_main
c
layout src
layout regs
</code></pre>
<blockquote>
<p><strong>练习 1</strong></p>
<p>启动带调试的QEMU,查看elf文件信息</p>
<ol>
<li>在rust_os-lab1目录下，打开两个终端，输入<code>make gdb</code>和<code>gdb -n -x .gdbinit</code>命令，可以分别打开带有GDB调试的QEMU以及GDB。</li>
<li>阅读参考资料1.，熟悉GDB的调试方法。</li>
<li>对比<code>readelf -h kernel/target/x86_64-unknown-none/debug/kernel</code>和GDB中的调试信息，找出<code>kernel/target/x86_64-unknown-none/debug/kernel</code>的入口地址发生了哪些变化。</li>
</ol>
</blockquote>
<h2 id="2-熟悉串口输出"><a class="header" href="#2-熟悉串口输出">2. 熟悉串口输出</a></h2>
<p>在示例代码<code>kernel/src/driver/serial.rs</code>中，实现了一个串口输出功能，支持通过调用<code>serial_print!</code>和<code>serial_println!</code>格式化输出至串口。</p>
<blockquote>
<p><strong>练习 2</strong> </p>
<ol>
<li>阅读示例代码中串口输出的实现（<code>serial_print!</code>和<code>serial_println!</code>）。</li>
</ol>
</blockquote>
<h2 id="3-显卡的字符模式与图形模式"><a class="header" href="#3-显卡的字符模式与图形模式">3. 显卡的字符模式与图形模式</a></h2>
<p>显卡最基本的两种工作模式是文字（也称为文本）模式和图形模式，在不同模式下，显卡对显存内容的解释是不同的。</p>
<p><strong>文本模式</strong>下，屏幕每行能显示80个字符，共25行。映射到显存，对应<code>0xB8000</code>到<code>0xBFFFF</code>这段物理地址，也被称为VGA字符缓冲区（VGA text buffer），也就是说，VGA字符缓冲区是一个25行、80列的二维数组。
<img src="pic/lab1-textbuffer-sc.png" alt="pic" /></p>
<p>其中，每个元素被称作字符单元，占显存两个字节大小，每个单元中的低8位表示字符的<a href="https://baike.baidu.com/item/ASCII/309296">ASCII编码</a>，高8位定义了字符的显示方式（字体，颜色，亮度，背景色，闪烁），如下图所示：</p>
<p><img src="pic/lab1-text-sc.png" alt="pic" /></p>
<p>通过写这些物理地址，就可以控制显示内容。</p>
<p>而在<strong>图形模式</strong>下，屏幕每行由多个像素点构成，要显示字符，则需要对每个字符涉及到的像素点进行绘制。Framebuffer是对显卡的硬件结构的一种抽象，可以直接通过对其读写对显存进行操作。示例代码<code>kernel/src/driver/fb.rs</code>文件中的<code>init</code>函数中在串口已经打印了<code>bootloader</code>传来的<code>FrameBuffer</code>信息，运行<code>make run</code>后可在终端看到如下内容：</p>
<p><img src="pic/lab1-framebuffer-sc.png" alt="图片" /></p>
<p>包括了显示缓冲区的起始地址<code>0x18000000000</code>和长度<code>0x300000</code>，屏幕宽<code>0x400</code>和高<code>0x300</code>的像素点数量，每个像素点需要所占的字节数<code>0x4</code>。通过简单的计算可验证缓冲区长度<code>0x300000 = 0x400 * 0x300 * 0x4</code>。像素格式<code>Bgr</code>的含义为一个像素的前三个字节分别代表蓝<code>B</code>绿<code>G</code>红<code>R</code>。</p>
<p>对像素点的绘制，可以通过确定像素在屏幕中的位置，算出相对显存起始位置的偏移，然后在对应字节写入相应的属性数值完成。在接下来的章节中，会提到如何用Rust的<code>noto_sans_mono_bitmap</code>库完成字符到像素矩阵的转换，降低开发难度。</p>
<blockquote>
<p><strong>练习 3</strong> </p>
<ol>
<li>阅读参考资料2.中使用VGA字符模式打印字符到屏幕的方式，学习函数<code>Writer.write_string()</code>，思考该函数是如何将一个字符串输出到屏幕上的，思考类型<code>Writer</code>的各个数据成员的作用。</li>
</ol>
</blockquote>
<p>本项目使用显卡的图形模式，在图形模式下输出字符时，需要访问显卡的 Framebuffer 。</p>
<h2 id="4-向-framebuffer-打印字符"><a class="header" href="#4-向-framebuffer-打印字符">4. 向 Framebuffer 打印字符</a></h2>
<p>Framebuffer，也叫帧缓冲，其内容对应于屏幕上的界面显示，可以将其简单理解为屏幕上显示内容对应的缓存。修改 Framebuffer 中的内容，即修改屏幕上的内容。Framebuffer 实际上包含了几个不同作用的缓存，比如颜色缓存、深度缓存等，在这几个缓存的共同作用下，形成了最终在屏幕上显示的图像。接下来我们就来实现在Framebuffer上的字符输出。</p>
<h3 id="41-包装到一个-rust-模块"><a class="header" href="#41-包装到一个-rust-模块">4.1. 包装到一个 Rust 模块</a></h3>
<p>在<code>kernel/src/driver/mod.rs</code>中定义了硬件驱动模块，我们在其中添加了 Framebuffer 模块：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/mod.rs中
#[macro_use]
// Framebuffer
pub mod fb;
<span class="boring">}</span></code></pre></pre>
<p>后续开发过程中如果有新添加的驱动模块，可以在该文件中进行扩展。</p>
<p>接下来完善<code>kernel/src/driver/fb.rs</code>文件，帧缓冲区实现的功能都保存在这个文件中。</p>
<h3 id="42-framebufferdriver"><a class="header" href="#42-framebufferdriver">4.2. FrameBufferDriver</a></h3>
<p>我们需要完善 FrameBufferDriver 的结构体，如下面程序中的 “TODO” 部分所示。结构体成员初始化方法可参考 <a href="https://docs.rs/bootloader_api/latest/bootloader_api/info/index.html">Framebuffer</a> 的相关实现。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/fb.rs中 

/// FrameBuffer驱动
pub struct FrameBufferDriver {
    // TODO：设计结构体需要的成员
}
<span class="boring">}</span></code></pre></pre>
<p>有了驱动结构体声明，接下来可以声明类型为 FrameBufferDriver 的全局变量，以及初始化它。我们使用了<code>bootloader_api</code> crate内置的 <a href="https://docs.rs/bootloader_api/latest/bootloader_api/info/index.html">Framebuffer</a> ，根据 <code>Bootinfo</code> 传入的信息创建 FrameBufferDriver 对象。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/fb.rs中 

use bootloader_api::info::FrameBuffer;
use spin::{Mutex, Once};

/// FrameBuffer驱动全局变量
pub static FRAME_BUFFER: Once&lt;Mutex&lt;FrameBufferDriver&gt;&gt; = Once::new();

/// 初始化FrameBuffer驱动
pub fn init&lt;'a&gt;(frame_buffer: &amp;'a mut FrameBuffer) {
    serial_println!(&quot;[Kernel] {:#x?}&quot;, frame_buffer);

    // 根据 frame_buffer (由 BootInfo 传入) 创建 FrameBufferDriver 对象
    FRAME_BUFFER.call_once(|| Mutex::new(FrameBufferDriver::new(frame_buffer)));
}
<span class="boring">}</span></code></pre></pre>
<p>现在，可以在<code>kernel/lib.rs</code>中初始化 FRAME_BUFFER：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/lib.rs中

/// 各类初始化函数
pub fn init&lt;'a&gt;(boot_info: &amp;'a mut bootloader_api::BootInfo) {
    ...

    // 初始化FrameBuffer
    driver::fb::init(boot_info.framebuffer.as_mut().unwrap());
}
<span class="boring">}</span></code></pre></pre>
<p>为了输出字符到屏幕，我们还需要创建一个<code>Printer</code>类型，并实现它的<code>Write</code>接口，以支持Rust提供的格式化宏，从而轻松地打印不同类型的变量，如整数或浮点数。为了支持它们，我们需要实现<a href="https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html">core::fmt::Write</a> trait；要实现它，需要提供的方法是<code>write_str</code>。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/fb.rs中 
use core::fmt;
use core::fmt::Write;
use x86_64::instructions::interrupts;

/// 辅助打印结构，主要实现Write trait
struct Printer;
impl Write for Printer {
    fn write_str(&amp;mut self, s: &amp;str) -&gt; fmt::Result {
        interrupts::without_interrupts(|| {
            FRAME_BUFFER
                .get()
                .and_then(|fb| Some(fb.lock().write_str(s)))
                .expect(&quot;Uninit frame buffer&quot;);
        });
        Ok(())
    }
}
<span class="boring">}</span></code></pre></pre>
<p><strong>注意</strong>，为了保证安全的内部可变性，实现<code>Write</code>接口时用到了<strong>自旋的互斥锁</strong>，有兴趣的同学可以对参考资料2.中的细节进行扩展阅读。</p>
<p>与参考资料2.的实现方法类似，可实现格式化打印至Framebuffer：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/fb.rs中 
/// 格式化打印至FrameBuffer（无换行）
#[macro_export]
macro_rules! fb_print {
    ($($arg:tt)*) =&gt; {
        $crate::driver::fb::print(format_args!($($arg)*))
    };
}

/// 格式化打印至FrameBuffer（有换行）
#[macro_export]
macro_rules! fb_println {
    () =&gt; ($crate::fb_print!(&quot;\n&quot;));
    ($fmt:expr) =&gt; ($crate::fb_print!(concat!($fmt, &quot;\n&quot;)));
    ($fmt:expr, $($arg:tt)*) =&gt; ($crate::fb_print!(
        concat!($fmt, &quot;\n&quot;), $($arg)*));
}
<span class="boring">}</span></code></pre></pre>
<p>因此，在FrameBufferDriver结构体实现中，至少需要添加<code>new</code>和<code>write_str</code>两个接口：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// TODO：设计结构体需要的接口，至少应包括new和write_str（前面的代码调用过）
impl FrameBufferDriver {
    /// 创建FrameBuffer驱动
    pub fn new&lt;'a&gt;(_framebuffer: &amp;'a mut FrameBuffer) -&gt; Self {
        // TODO：新建FrameBuffer驱动
        Self {}
    }

    /// 写字符串
    pub fn write_str(&amp;mut self, _s: &amp;str) {
        // TODO
    }

    /// 其它私有函数
    // TODO
}
<span class="boring">}</span></code></pre></pre>
<p>函数<code>write_str</code>的实现涉及字符的逐个输出、字符到像素矩阵转化以及像素矩阵在屏幕上的显示。</p>
<p>实现过程中最困难的地方是一个字符到像素矩阵的转化，我们在<code>kernel/tests/chr2pixels.rs</code>文件中使用了<a href="https://crates.io/crates/noto-sans-mono-bitmap">noto_sans_mono_bitmap</a>库提供的相关功能，展示了字符<code>'E'</code>转化为像素矩阵的方法，可作为参考，运行<code>make test</code>可看结果。</p>
<p><img src="pic/lab1-test-sc.png" alt="pic" /></p>
<p>接下来，我们将对其它需要实现的与屏幕输出相关的私有函数进行说明。</p>
<h3 id="43-framebufferdriver-的私有函数"><a class="header" href="#43-framebufferdriver-的私有函数">4.3. FrameBufferDriver 的私有函数</a></h3>
<ul>
<li>
<p>换行（newline）：是为了在格式化输出中支持<code>fb_println！()</code>。</p>
</li>
<li>
<p>清屏（clear）：系统启动初期屏幕上输出的是 boot 阶段获取的信息，在输出我们自己的字符串之前，应对Framebuffer中的内容进行清空。</p>
</li>
</ul>
<p>上述函数的实现类似于参考资料2.中<code>Writer</code>类型的相关函数。</p>
<h3 id="44-在屏幕上输出hello-world字符串"><a class="header" href="#44-在屏幕上输出hello-world字符串">4.4. 在屏幕上输出“Hello world”字符串</a></h3>
<p>在完成以上所有准备后，现在请启用<code>kernel/main.rs</code>里<code>fb_println!</code>语句：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/main.rs中 
fn kernel_main(boot_info: &amp;'static mut BootInfo) -&gt; ! {
    kernel::init(boot_info);

    kernel::serial_println!(&quot;Hello world&quot;);

    // TODO：使用fb_print宏输出hello至FrameBuffer
    // kernel::fb_println!(&quot;Hello world, my name is ...&quot;);

    kernel::hlt_loop();
}
<span class="boring">}</span></code></pre></pre>
<p>这样我们就可以在屏幕上输出“Hello world, my name is ...”字符串，最终效果如下：</p>
<p><img src="pic/lab1-finish-sc.png" alt="pic" /></p>
<h2 id="小结"><a class="header" href="#小结">小结</a></h2>
<p>请在完成实验之后提交实验报告和所有源码，实验报告应记录实验过程和主要画面。</p>
<p>通过本实验，相信大家能够熟悉GDB调试方法，分析ELF格式文件信息；能够熟悉Rust语法特性，了解Rust库的调用方法；熟悉项目的开发模式，并对显卡的不同输出方式有了更深的理解。</p>
<h2 id="下一个实验"><a class="header" href="#下一个实验">下一个实验</a></h2>
<p>实现串口和鼠标中断。请提前复习CPU异常处理和硬件中断的相关知识点。</p>
<h2 id="参考资料-1"><a class="header" href="#参考资料-1">参考资料</a></h2>
<ol>
<li>GDB手册：<a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/">https://sourceware.org/gdb/current/onlinedocs/gdb.html/</a></li>
<li>VGA字符模式打印实现
<ul>
<li>原版：<a href="https://os.phil-opp.com/vga-text-mode/">https://os.phil-opp.com/vga-text-mode/</a></li>
<li>中文翻译：<a href="https://www.bookstack.cn/read/writing-an-os-in-rust/03-vga-text-mode.md">https://www.bookstack.cn/read/writing-an-os-in-rust/03-vga-text-mode.md</a></li>
</ul>
</li>
</ol>
<div style="break-before: page; page-break-before: always;"></div><h1 id="实验2实现串口和鼠标中断"><a class="header" href="#实验2实现串口和鼠标中断">实验2：实现串口和鼠标中断</a></h1>
<h2 id="实验导读-1"><a class="header" href="#实验导读-1">实验导读</a></h2>
<p>大家已经完成了向Framebuffer输出字符的功能，相信已经对项目的整体结构、Rust的语法更加熟悉。本次实验需要对系统功能进行扩展，提供串口中断和鼠标中断处理(*鼠标中断为选做)，并且实现在QEMU终端和串口输入输出端各实现一个简单的命令解释器。</p>
<p>本实验提供一份基础代码，还有部分需要实验者自己补充，已在代码中用“TODO”标出。</p>
<ul>
<li><a href="lab_2.html#%E6%B7%BB%E5%8A%A0cpu%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E6%A8%A1%E5%9D%97">添加CPU异常处理模块</a>
<ul>
<li><a href="lab_2.html#%E4%B8%AD%E6%96%AD%E6%8F%8F%E8%BF%B0%E7%AC%A6%E8%A1%A8">中断描述符表</a></li>
<li><a href="lab_2.html#%E5%8C%85%E8%A3%85%E5%88%B0rust%E6%A8%A1%E5%9D%97">包装到Rust模块</a></li>
<li><a href="lab_2.html#%E6%B7%BB%E5%8A%A0%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B">添加测试用例</a></li>
</ul>
</li>
<li><a href="lab_2.html#%E6%B7%BB%E5%8A%A0%E6%97%B6%E9%92%9F%E5%92%8C%E9%94%AE%E7%9B%98%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86">添加时钟和键盘中断处理</a>
<ul>
<li><a href="lab_2.html#8259%E5%8F%AF%E7%BC%96%E7%A8%8B%E4%B8%AD%E6%96%AD%E6%8E%A7%E5%88%B6%E5%99%A8">8259可编程中断控制器</a></li>
<li><a href="lab_2.html#%E5%90%AF%E7%94%A8%E4%B8%AD%E6%96%AD">启用中断</a></li>
<li><a href="lab_2.html#%E8%AE%A1%E6%97%B6%E5%99%A8%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86">计时器中断处理</a></li>
<li><a href="lab_2.html#%E9%94%AE%E7%9B%98%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86">键盘中断处理</a></li>
</ul>
</li>
<li><a href="lab_2.html#%E6%B7%BB%E5%8A%A0%E4%B8%B2%E5%8F%A3%E4%B8%AD%E6%96%AD%E5%92%8C%E9%BC%A0%E6%A0%87%E4%B8%AD%E6%96%AD%E5%8A%9F%E8%83%BD%E9%BC%A0%E6%A0%87%E4%B8%AD%E6%96%AD%E4%B8%BA%E9%80%89%E5%81%9A">添加串口中断和鼠标中断功能（*鼠标中断为选做）</a></li>
<li><a href="lab_2.html#%E5%91%BD%E4%BB%A4%E8%A7%A3%E9%87%8A%E5%99%A8%E5%88%9D%E6%AD%A5%E5%AE%9E%E7%8E%B0">命令解释器初步实现</a></li>
<li><a href="lab_2.html#%E5%B0%8F%E7%BB%93">小结</a></li>
<li><a href="lab_2.html#%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%AE%9E%E9%AA%8C">下一个实验</a></li>
<li><a href="lab_2.html#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</a></li>
</ul>
<h3 id="添加cpu异常处理模块"><a class="header" href="#添加cpu异常处理模块">添加CPU异常处理模块</a></h3>
<p>CPU异常在很多情况下都有可能发生，比如访问无效的内存地址，或者在除法运算里除以0。当异常发生后，CPU会中断当前的工作，并立即根据异常类型调用对应的错误处理函数。</p>
<h4 id="中断描述符表"><a class="header" href="#中断描述符表">中断描述符表</a></h4>
<p>要捕捉CPU异常，我们需要设置一个 <em>中断描述符表</em> (<em>Interrupt Descriptor Table</em>, IDT)，用来捕获每一个异常。由于硬件层面会不加验证的直接使用，所以我们需要根据预定义格式直接写入数据。</p>
<p>我们直接使用<code>x86_64</code> crate 内置的 <a href="https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html"><code>InterruptDescriptorTable</code> 结构</a>对异常进行捕获。</p>
<h4 id="包装到rust模块"><a class="header" href="#包装到rust模块">包装到Rust模块</a></h4>
<p>首先在<code>kernel/src/driver/mod.rs</code>中创建中断描述符表模块：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/mod.rs中

// 中断描述符表
pub mod idt;
<span class="boring">}</span></code></pre></pre>
<p>创建<code>kernel/src/driver/idt.rs</code>，加入初始化函数，并使用<a href="https://www.felixcloutier.com/x86/lgdt:lidt">lidt</a>指令进行装载（<code>x86_64</code>的<code>InterruptDescriptorTable</code>结构提供了[load]函数）。<strong>注意</strong>，这里涉及到<strong>Rust语言生命周期</strong>、<strong>懒加载</strong>等概念，有兴趣的同学可以对参考资料1.进行扩展阅读。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中
use spin::Lazy;
use x86_64::structures::idt::InterruptDescriptorTable;

static IDT: Lazy&lt;InterruptDescriptorTable&gt; = Lazy::new(|| {
    let idt = InterruptDescriptorTable::new();
    idt
});

/// 初始化中断描述符表
pub fn init() {
    IDT.load();
}
<span class="boring">}</span></code></pre></pre>
<p>接下来添加处理函数，首先添加<code>breakpoint exception</code>作为一个测试途径。我们创建一个简单的<code>breakpoint_handler</code>方法并加入IDT中：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中
use spin::Lazy;
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};

static IDT: Lazy&lt;InterruptDescriptorTable&gt; = Lazy::new(|| {
    let mut idt = InterruptDescriptorTable::new();
    idt.breakpoint.set_handler_fn(breakpoint_handler);
    idt
});

/// 初始化中断描述符表
pub fn init() {
    IDT.load();
}

extern &quot;x86-interrupt&quot; fn breakpoint_handler(stack_frame: InterruptStackFrame) {
    fb_println!(&quot;EXCEPTION: BREAKPOINT\n{:#?}&quot;, stack_frame);
}
<span class="boring">}</span></code></pre></pre>
<p>同时，为了能够手动启用<code>x86-interrupt</code>特性，在<code>kernel/lib.rs</code>中加入<code>#![feature(abi_x86_interrupt)]</code>开关。</p>
<p>现在，可以在<code>kernel/lib.rs</code>中初始化中断描述符表：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/lib.rs中

/// 各类初始化函数
pub fn init&lt;'a&gt;(boot_info: &amp;'a mut bootloader_api::BootInfo) {
    ...

    // 初始化中断描述符表
    driver::idt::init();
}
<span class="boring">}</span></code></pre></pre>
<h4 id="添加测试用例"><a class="header" href="#添加测试用例">添加测试用例</a></h4>
<p><code>breakpoint exception</code>的唯一目的就是在<code>int3</code>指令执行时暂停程序运行，现在我们可以添加一个测试用例来确保以上工作可以顺利运行。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中

#[test_case]
fn test_breakpoint() {
    x86_64::instructions::interrupts::int3();
}
<span class="boring">}</span></code></pre></pre>
<p>在<code>kernel</code>文件夹下运行<code>cargo test --lib</code>命令后可看到：</p>
<p><img src="pic/lab2-breakpointtest-sc.png" alt="pic" /></p>
<blockquote>
<p><strong>练习 1</strong></p>
<ol>
<li>
<p>参考<a href="https://os.phil-opp.com/">blog_os</a>教程的<a href="https://os.phil-opp.com/cpu-exceptions/">CPU Exceptions</a>章节，并阅读<code>kernel/src/driver/idt.rs</code>中的代码，理解上述过程。</p>
</li>
<li>
<p>参考<a href="https://os.phil-opp.com/">blog_os</a>教程的<a href="https://os.phil-opp.com/double-fault-exceptions/">Double Fautls</a>章节，并阅读<code>kernel/src/driver/idt.rs</code>中的代码，梳理double fault异常处理流程。</p>
</li>
</ol>
</blockquote>
<h3 id="添加时钟和键盘中断处理"><a class="header" href="#添加时钟和键盘中断处理">添加时钟和键盘中断处理</a></h3>
<p>中断处理和异常处理很类似，只需要在中断描述符表中加入一个新条目即可。而CPU对中断的获取则通过统一的 <em>中断控制器</em> 代理。</p>
<h4 id="8259可编程中断控制器"><a class="header" href="#8259可编程中断控制器">8259可编程中断控制器</a></h4>
<p>我们直接使用<a href="https://docs.rs/pic8259/0.10.1/pic8259/">pic8259</a> crate对PIC进行配置和编程。首先将crate作为依赖加入工程中：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/Cargo.toml中

[dependencies]
pic8259 = &quot;0.10&quot;
<span class="boring">}</span></code></pre></pre>
<p>并在<code>kernel/src/driver/mod.rs</code>中声明模块：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/mod.rs中

// 中断控制器
pub mod pic;
<span class="boring">}</span></code></pre></pre>
<p>然后使用<a href="https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html">ChainedPics</a>映射PIC的布局：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

use pic8259::ChainedPics;
use spin::Mutex;

/// 一级PIC偏移
pub const PIC_1_OFFSET: u8 = 32;

/// 二级PIC偏移
pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;

/// 级联PIC驱动全局变量
pub static PICS: Mutex&lt;ChainedPics&gt; =
    Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });
<span class="boring">}</span></code></pre></pre>
<p>以上，我们成功将PIC的中断编号范围设定为了32–47。（PIC默认的配置其实是无法使用的，因为它仅仅是将0-15之间的中断向量编号发送给了CPU，然而这些编号已经用在了CPU的异常编号中了，比如8号代指 double fault 异常。要修复这个错误，我们需要对PIC中断序号进行重映射，新的序号只需要避开已被定义的CPU异常即可，CPU定义的异常数量有32个，所以通常会使用32-47这个区段）</p>
<p>同时，使用 <code>Mutex</code> 容器包裹了 <code>ChainedPics</code>，这样就可以通过（<a href="https://docs.rs/spin/0.5.2/spin/struct.Mutex.html#method.lock"><code>lock</code> 函数</a>）拿到被定义为安全的变量修改权限。</p>
<p>现在，我们就可以初始化 8259 PIC 配置了：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

/// 初始化中断控制器
pub fn init() {
    unsafe { PICS.lock().initialize() };
}
<span class="boring">}</span></code></pre></pre>
<p>并且在<code>kernel/lib.rs</code>中进行调用：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/lib.rs中

/// 各类初始化函数
pub fn init&lt;'a&gt;(boot_info: &amp;'a mut bootloader_api::BootInfo) {
    ...

    // 初始化中断控制器
    driver::pic::init();
}
<span class="boring">}</span></code></pre></pre>
<h4 id="启用中断"><a class="header" href="#启用中断">启用中断</a></h4>
<p>CPU现在还不能监听来自中断控制器的信息，因为CPU配置中中断处于禁用状态，因此，在初始化函数中增加如下方法进行启用：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

/// 初始化中断控制器
pub fn init() {
    unsafe { PICS.lock().initialize() };

    // CPU端开启中断
    x86_64::instructions::interrupts::enable();
}
<span class="boring">}</span></code></pre></pre>
<h4 id="计时器中断处理"><a class="header" href="#计时器中断处理">计时器中断处理</a></h4>
<p>我们需要增加对计时器中断信号的处理函数（否则运行后将抛出double fault异常）。首先，使用枚举类型<code>InterruptIndex</code>存储计时器的中断序号，为以后加入更多的中断枚举提供可扩展的编程风格。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
/// 外设中断编号
pub enum InterruptIndex {
    /// 时钟
    Timer = PIC_1_OFFSET,
}
<span class="boring">}</span></code></pre></pre>
<p>接下来在中断描述符表中为计时器中断添加一个处理函数<code>timer_interrupt_handler</code>，处理函数返回“中断结束”信号，该信号通知控制器中断已处理，系统可以接收下一个中断：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中
use crate::driver::pic::{notify_eoi, InterruptIndex};

static IDT: Lazy&lt;InterruptDescriptorTable&gt; = Lazy::new(|| {
    let mut idt = InterruptDescriptorTable::new();
    [...]
    idt[InterruptIndex::Timer as usize].set_handler_fn(timer_interrupt_handler);
    idt
});

extern &quot;x86-interrupt&quot; fn timer_interrupt_handler(_stack_frame: InterruptStackFrame) {
    notify_eoi(InterruptIndex::Timer as u8);
}
<span class="boring">}</span></code></pre></pre>
<p>中断结束信号的处理函数在<code>kernel/src/driver/pic.rs</code>中：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

/// 通知完成中断
pub fn notify_eoi(irq: u8) {
    unsafe {
        PICS.lock().notify_end_of_interrupt(irq);
    }
}
<span class="boring">}</span></code></pre></pre>
<h4 id="键盘中断处理"><a class="header" href="#键盘中断处理">键盘中断处理</a></h4>
<p>键盘中断处理的添加流程与计时器中断流程类似，首先是增加中断序号：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
/// 外设中断编号
pub enum InterruptIndex {
    /// 时钟
    Timer = PIC_1_OFFSET,
    /// 键盘
    Keyboard,
}
<span class="boring">}</span></code></pre></pre>
<p>然后是增加中断处理函数<code>keyboard_interrupt_handler</code>：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中
use crate::driver::pic::{notify_eoi, InterruptIndex};
use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1};

static IDT: Lazy&lt;InterruptDescriptorTable&gt; = Lazy::new(|| {
    let mut idt = InterruptDescriptorTable::new();
    [...]
    idt[InterruptIndex::Keyboard as usize].set_handler_fn(keyboard_interrupt_handler);
    idt
});

extern &quot;x86-interrupt&quot; fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {
    use x86_64::instructions::port::Port;
    let mut port = Port::new(0x60);
    let scancode: u8 = unsafe { port.read() };
    let mut keyboard = Keyboard::new(
        ScancodeSet1::new(),
        layouts::Us104Key,
        HandleControl::Ignore,
    );

    if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
        if let Some(key) = keyboard.process_keyevent(key_event) {
            match key {
                DecodedKey::Unicode(character) =&gt; {
                    fb_print!(&quot;{}&quot;, character);
                }
                DecodedKey::RawKey(_key) =&gt; {
                    fb_print!(&quot;{:?}&quot;, key);
                }
            }
        }
    }
    notify_eoi(InterruptIndex::Keyboard as u8);
}
<span class="boring">}</span></code></pre></pre>
<p>直接<code>make run</code>会发现键盘中断没有响应，因为教程中默认使用BIOS模式启动，PIC默认打开键盘中断；我们的内核默认使用UEFI模式启动，PIC默认关闭了键盘中断，可使用<code>make run boot=bios</code>命令运行，测试键盘中断。</p>
<p>这里对<strong>键盘中断</strong>、<strong>扫描码读取</strong>、<strong>扫描码转义</strong>的过程说明请参考<a href="https://os.phil-opp.com/">blog_os</a>教程的<a href="https://os.phil-opp.com/hardware-interrupts/">Hardware Interrupts</a>章节。</p>
<blockquote>
<p><strong>练习 2</strong></p>
<p>示例代码中的键盘中断实现未支持删除键的显示，请思考如何捕捉这一输入并在屏幕上实现删除字符的效果。</p>
</blockquote>
<h3 id="添加串口中断和鼠标中断功能鼠标中断为选做"><a class="header" href="#添加串口中断和鼠标中断功能鼠标中断为选做">添加串口中断和鼠标中断功能（*鼠标中断为选做）</a></h3>
<p>添加串口和鼠标中断的过程与计时器、键盘中断过程类似，首先需要增加中断序号：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
/// 外设中断编号
pub enum InterruptIndex {
    /// 时钟
    Timer = PIC_1_OFFSET,
    /// 键盘
    Keyboard,
    // TODO
}
<span class="boring">}</span></code></pre></pre>
<p>然后是增加中断处理函数：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中
use crate::driver::pic::{notify_eoi, InterruptIndex};

static IDT: Lazy&lt;InterruptDescriptorTable&gt; = Lazy::new(|| {
    let mut idt = InterruptDescriptorTable::new();
    [...]
    // TODO
    idt
});

// TODO: 分别为串口和鼠标中断实现处理函数
extern &quot;x86-interrupt&quot; fn [...](_stack_frame: InterruptStackFrame) {
    
}
<span class="boring">}</span></code></pre></pre>
<p>串口中断的实现可以使用示例代码<code>kernel/src/driver/serial.rs</code>中的相关函数帮助实现，实现效果为可以在终端进行键盘输入并看到相应字符，如下图红色方框中字符所示：
<img src="pic/lab2-serial-sc.png" alt="pic" /></p>
<p>鼠标设备驱动的原理可以从<a href="https://wiki.osdev.org/PS/2_Mouse">资料</a>中获取，我们建议使用社区中已有的第三方库<a href="https://crates.io/crates/ps2-mouse">ps2-mouse</a>帮助实现，通过阅读参考库的示例代码和文档，方便地实现鼠标设备的支持和相关中断处理，实现效果为可以展示系统对鼠标事件（移动、点击等）的感知（最好能做出图形界面的效果，可以看到鼠标的移动轨迹，点击不同按键可以有不同效果等），参考实现效果如下图：
<img src="pic/lab2-finish-sc.png" alt="pic" /></p>
<blockquote>
<p><strong>练习 3</strong>
仿照已有的时钟中断和键盘中断处理流程，增加串口中断和鼠标中断（*鼠标中断为选做）。</p>
<p>提示</p>
<ul>
<li>一个中断一般涉及三个参与者：CPU、中断控制器（PIC）、设备，三者均需打开中断:
<ul>
<li>CPU中断总开关在<code>kernel/src/driver/pic.rs</code>文件的<code>init</code>函数中打开</li>
<li>PIC的中断开启方法参考我们用的<a href="https://crates.io/crates/pic8259">pic8259</a>库的<a href="https://docs.rs/pic8259/0.10.4/pic8259/struct.ChainedPics.html">手册</a></li>
<li>设备方面，串口设备已在初始化阶段打开中断，鼠标设备目前我们还没有相关驱动</li>
</ul>
</li>
</ul>
</blockquote>
<h3 id="命令解释器初步实现"><a class="header" href="#命令解释器初步实现">命令解释器初步实现</a></h3>
<p>大家在实验的过程中使用了命令解释器和电脑进行交互，包括查看文件、编译项目等等。目前我们的实验操作系统也已经拥有了显示功能、键盘输入功能并且支持串口中断，那么大家可以开始在这个实验操作系统上实现一个简单的命令解释器，并进行一些交互：一方面可以在QEMU上进行命令的解释交互，另一方面可以在串口终端上进行调试信息的输出。本次实验我们只会实现一个非常简单的概念命令解释器，随着后续操作系统功能的增加，再对这个命令解释器的功能进行逐步完善。</p>
<p>假设我们已经实现了一个shell模块（<code>crate::shell::shell_input</code>），那么在键盘输入的时候，就可以对输入的字符进行解析，用这个功能模块替换之前在显示器上单纯的输出：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>extern &quot;x86-interrupt&quot; fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {
    [...]

    if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
        if let Some(key) = keyboard.process_keyevent(key_event) {
            match key {
                DecodedKey::Unicode(character) =&gt; {
                    // 替换了之前fb_print!
                    // TODO: 请创建一个shell模块并实现shell_input功能
                    crate::shell::shell_input(character);
                }
                DecodedKey::RawKey(_key) =&gt; {
                    fb_print!(&quot;{:?}&quot;, key);
                }
            }
        }
    }
    [...]
}
<span class="boring">}</span></code></pre></pre>
<p>接下来，我们需要在<code>kernel</code>的根目录下新建一个<code>shell.rs</code>文件，实现<code>shell_input</code>功能。在实现过程中，如果大家引入了alloc库并且使用了String类型，会得到如下报错信息：</p>
<p><img src="pic/lab2-error.png" alt="pic" /></p>
<p>这是由于目前在我们的操作系统没有使用Rust的标准库（<code>#![no_std]</code>），而我们也还没有实现全局的内存分配器，因此无法使用这一动态数据类型。在实验3中，我们会增加内存管理的相关功能，并且将用新的方式实现一个更灵活的命令解释器。</p>
<p>在此之前，大家可以使用rust中的数组代替字符类型进行字符串的记录和匹配。可以声明一个全局的数组记录键盘的输入。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>/// 处理键盘输入
pub fn shell_input(chr: char) {
    
    // TODO: 增加字符解析功能，实现以下功能
    /*
    1. 输入help+回车，显示帮助信息
    2. 输入clear+回车，清空屏幕显示
    3. 输入exit+回车，退出qemu终端
    4. 输入reboot+回车，重启终端
    5. 输入其他未定义命令+回车，弹出错误提示信息
    6. 其他功能：每行命令输出前有提示前缀；可以回退删除打错的字符
    */

}
<span class="boring">}</span></code></pre></pre>
<blockquote>
<p><strong>提示</strong></p>
<ul>
<li>退出：在kernel/src/test.rs中我们定义了exit_qemu函数</li>
<li>重启：我们使用的x86_64库没有封装重启系统函数，我们可以手写以下指令实现：</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>core::arch::asm!(
    &quot;cli
    mov al, 0xfe
    out 0x64, al&quot;
);
<span class="boring">}</span></code></pre></pre>
</blockquote>
<p>完成效果如下图所示:</p>
<p><img src="pic/lab2-shell-final.png" alt="pic" /></p>
<p>除此之外，由于实现了串口中断和输入输出功能，我们同样可以在串口重定向的终端输入简单的调试命令获取相应的信息。实现方法和<code>shell_input</code>类似，大家可以在串口中断时对输入的字符串进行解析：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>extern &quot;x86-interrupt&quot; fn com1_interrupt_handler(_stack_frame: InterruptStackFrame) {
    [...]
    // 替换了之前的serial_print！
    // TODO: 请在shell模块并实现serial_shell_input功能
    crate::shell::serial_shell_input(chr);
    [...]
}
<span class="boring">}</span></code></pre></pre>
<p>并在<code>shell.rs</code>文件，实现<code>serial_shell_input</code>函数，完成以下基本调试功能:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>/// 解析串口输入
pub fn serial_shell_input(chr: char) {
    
    // TODO: 增加字符解析功能，实现以下功能
    /*
    1. 输入time+回车，显示系统目前时钟中断次数
    2. 输入其他未定义命令+回车，弹出错误提示信息
    3. 其他功能：每行命令输出前有提示前缀；可以回退删除打错的字符
    */

}
<span class="boring">}</span></code></pre></pre>
<p>完成效果如下图所示:</p>
<p><img src="pic/lab2-serial-shell-sc.png" alt="pic" /></p>
<blockquote>
<p><strong>练习 4</strong></p>
<p>在QEMU终端和串口输入端各增加一个简单的命令解释器，完成代码TODO部分提到的基本功能。</p>
</blockquote>
<h2 id="小结-1"><a class="header" href="#小结-1">小结</a></h2>
<p>请在完成实验之后提交实验报告和所有源码，实验报告应记录实验过程和主要画面。</p>
<p>不同于Linux开发，在基于Rust的内核开发中，我们可以使用大量第三方库，学会库的使用会极大提升我们的开发效率。</p>
<p>通过本实验，相信大家能够对CPU异常处理过程和硬件中断过程有更加深入的理解。</p>
<h2 id="下一个实验-1"><a class="header" href="#下一个实验-1">下一个实验</a></h2>
<p>实现物理页帧分配器及物理页帧到虚拟页的映射。请提前复习内存管理相关知识点。</p>
<h2 id="参考资料-2"><a class="header" href="#参考资料-2">参考资料</a></h2>
<ol>
<li>blog_os中CPU异常章节：<a href="https://os.phil-opp.com/cpu-exceptions/">https://os.phil-opp.com/cpu-exceptions/</a></li>
<li>blog_os中硬件中断章节：<a href="https://os.phil-opp.com/hardware-interrupts/">https://os.phil-opp.com/hardware-interrupts/</a></li>
<li>PIC8259相关链接：
第三方库:<a href="https://crates.io/crates/pic8259">https://crates.io/crates/pic8259</a>
手册:<a href="https://docs.rs/pic8259/0.10.4/pic8259/struct.ChainedPics.html">https://docs.rs/pic8259/0.10.4/pic8259/struct.ChainedPics.html</a></li>
</ol>
<div style="break-before: page; page-break-before: always;"></div><h1 id="实验3内存管理"><a class="header" href="#实验3内存管理">实验3：内存管理</a></h1>
<h2 id="实验导读-2"><a class="header" href="#实验导读-2">实验导读</a></h2>
<p>大家已经完成了串口中断和鼠标中断的处理，本次实验需要在系统增加内存管理相关功能，从而支持操作系统让不同应用程序在同时运行时可以安全的共同使用物理内存资源。</p>
<p>本实验提供一份基础代码，还有部分需要实验者自己补充，已在代码中用“TODO”标出。</p>
<ul>
<li><a href="lab_3.html#%E5%BC%95%E5%AF%BC%E5%8A%A0%E8%BD%BD%E7%A8%8B%E5%BA%8F%E7%9A%84%E9%85%8D%E7%BD%AE">引导加载程序的配置</a></li>
<li><a href="lab_3.html#%E7%89%A9%E7%90%86%E9%A1%B5%E5%B8%A7%E5%88%86%E9%85%8D%E5%99%A8">物理页帧分配器</a></li>
<li><a href="lab_3.html#%E7%89%A9%E7%90%86%E9%A1%B5%E5%B8%A7%E5%88%B0%E8%99%9A%E6%8B%9F%E9%A1%B5%E7%9A%84%E6%98%A0%E5%B0%84">物理页帧到虚拟页的映射</a></li>
<li><a href="lab_3.html#%E5%AE%9E%E7%8E%B0%E5%A0%86%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E5%99%A8%E9%80%89%E5%81%9A">实现堆内存分配器（*选做）</a></li>
<li><a href="lab_3.html#%E5%91%BD%E4%BB%A4%E8%A7%A3%E9%87%8A%E5%99%A8%E7%9A%84%E9%87%8D%E6%96%B0%E5%AE%9E%E7%8E%B0%E9%80%89%E5%81%9A">命令解释器的重新实现（*选做）</a></li>
<li><a href="lab_3.html#%E5%B0%8F%E7%BB%93">小结</a></li>
<li><a href="lab_3.html#%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%AE%9E%E9%AA%8C">下一个实验</a></li>
<li><a href="lab_3.html#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</a></li>
</ul>
<h3 id="引导加载程序的配置"><a class="header" href="#引导加载程序的配置">引导加载程序的配置</a></h3>
<p>大家在实验1中已经了解了内核在加载之后从低地址转移到高地址的过程，这样将内核数据加载到高地址的主要原因是希望在后续内核功能丰富之后仍然有较大的生长空间。但是，当我们需要实现内存管理中访问页表的操作时，由于内核已经加载到了虚拟地址，因此需要额外的支持来实现从内核访问页表的过程。</p>
<p>引导加载程序创建了内核运行的页表，有权访问页表，因此也可以创建我们需要的任何映射，我们可以通过配置bootloader，将整个物理内存映射到虚拟地址空间中的某个位置。因此，内核可以访问所有物理内存，并且可以遵循 “映射完整物理内存” 方法。配置方式如下所示：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/main.rs中
use bootloader_api::{config::Mapping, BootloaderConfig};

/// 物理地址偏移
pub const KERNEL_PHY_OFFSET: usize = 0xFFFF_FF80_0000_0000;

/// bootloader配置
pub static BOOTLOADER_CONFIG: BootloaderConfig = {
    let mut config = BootloaderConfig::new_default();
    config.mappings.physical_memory = Some(Mapping::FixedAddress(KERNEL_PHY_OFFSET as _));
    config
};

// 使用bootloader_api库提供的宏声明内核入口，传入config配置信息
bootloader_api::entry_point!(kernel_main, config = &amp;BOOTLOADER_CONFIG);
<span class="boring">}</span></code></pre></pre>
<p>引导加载程序之后会传递一个<code>引导信息BootInfo</code>结构给内核，从而传达虚拟地址范围。</p>
<h3 id="物理页帧分配器"><a class="header" href="#物理页帧分配器">物理页帧分配器</a></h3>
<p>现在我们可以访问物理内存了，首先来实现一个物理页帧分配器。新建一个内存管理模块，利用<code>bootinfo</code>的信息对其进行初始化：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/lib.rs中
mod mem;

/// 各类初始化函数
pub fn init&lt;'a&gt;(boot_info: &amp;'a mut bootloader_api::BootInfo) {
    [...]
    // 初始化内存管理
    mem::init(&amp;mut boot_info.memory_regions);
}
<span class="boring">}</span></code></pre></pre>
<p>接下来引入物理页帧模块：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/mem/mod.rs中
mod frame_allocator;
<span class="boring">}</span></code></pre></pre>
<p>在<code>kernel/src/mem/frame_allocator.rs</code>中，对物理页帧进行初始化。通过从<code>bootloader</code>传递来的<code>info</code>，我们可以获得虚拟地址的范围<code>memory_regions</code>：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>use bootloader_api::info::MemoryRegions;

/// 初始化页帧分配器
pub fn init_frame_allocator&lt;'a&gt;(memory_regions: &amp;'a mut MemoryRegions) {
    serial_println!(&quot;[Kernel] Memory regions:&quot;);
    for region in memory_regions.into_iter() {
        serial_println!(&quot;    {:x?}&quot;, region);
    }
    [...]
}
<span class="boring">}</span></code></pre></pre>
<p>打印出的结果如下所示：
<img src="lab3_memory_region.png" alt="pic" /></p>
<p>在上图中，有标记为<code>Usable</code>的区域为可用内存，大家可以参考blog_os中<a href="https://www.bookstack.cn/read/writing-an-os-in-rust/09-paging-implementation.md">分页实现</a>中的方法将内存映射转换为可用帧的迭代器。</p>
<p>接下来，我们需要设计页帧分配器结构体，并创建全局变量，在<code>init_frame_allocator</code>函数中初始化全局变量，并在<code>allocate_frame</code>、<code>allocate_frame_cont</code>、<code>deallocate_frame</code>函数中使用全局变量完成相关操作,补全以下代码中的TODO部分。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>/// 初始化页帧分配器
pub fn init_frame_allocator&lt;'a&gt;(memory_regions: &amp;'a mut MemoryRegions) {
    [...]
    // TODO
}

/// 申请一个可用页帧，返回首地址
pub fn allocate_frame() -&gt; Option&lt;usize&gt; {
    // TODO
    None
}

/// 申请一组连续的页帧，返回第一个页帧的首地址
pub fn allocate_frame_cont(_size: usize) -&gt; Option&lt;usize&gt; {
    // TODO
    None
}

/// 释放给定地址的物理页帧
pub fn deallocate_frame(_frame: usize) {
    // TODO
}
<span class="boring">}</span></code></pre></pre>
<blockquote>
<p><em><strong>练习1</strong></em></p>
<p>管理<code>bootloader</code>给定的可用物理地址范围，实现物理页帧的分配和回收，完成后可设计测试用例验证分配器正确性。</p>
</blockquote>
<h3 id="物理页帧到虚拟页的映射"><a class="header" href="#物理页帧到虚拟页的映射">物理页帧到虚拟页的映射</a></h3>
<p>接下来我们将实现物理页帧到虚拟页的映射。首先在<code>kernel/src/mem/page_table.rs</code>中，实现<code>page_table.map(vaddr, paddr, flags)</code>函数功能：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/mem/page_table.rs中

use x86_64::{instructions::tlb, structures::paging::PageTableFlags};

/// 主要实现页表映射相关功能
#[derive(Debug, Default)]
#[allow(dead_code)]
pub struct PageTable {
    /// 页表的基地址
    paddr: usize,
}

#[allow(dead_code)]
impl PageTable {
    /// 使用已有物理页帧作为基地址创建页表
    pub fn new(paddr: usize) -&gt; Self {
        PageTable { paddr }
    }

    /// 获取页表基地址
    pub fn paddr(&amp;self) -&gt; usize {
        self.paddr
    }

    /// 将paddr所在物理页帧映射至虚拟页page，并设置标记flags
    pub fn map(&amp;self, _vaddr: usize, _paddr: usize, _flags: PageTableFlags) {
        // TODO

        // 完成映射后刷新tlb
        tlb::flush_all();
    }
}
<span class="boring">}</span></code></pre></pre>
<p>在实现<code>map</code>函数的过程中，需要访问给定物理地址的内容，但我们内核运行于虚存空间中（<code>bootloader</code>为内核创建了地址空间，并加载映射内核），为访问物理页，<code>bootloader</code>将物理地址映射到以<code>0xFFFF_FF80_0000_0000</code>为偏移的虚拟地址，代码中的<code>phys_to_virt</code>函数可以帮助获取一个物理地址对应的虚拟地址，读写此虚拟地址等同于读写物理地址。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/mem/mod.rs中
/// 物理地址偏移
pub const KERNEL_PHY_OFFSET: usize = 0xFFFF_FF80_0000_0000;

/// 物理地址转虚拟地址
pub fn phys_to_virt(paddr: usize) -&gt; usize {
    KERNEL_PHY_OFFSET + paddr
}
<span class="boring">}</span></code></pre></pre>
<p>接下俩，我们可以在<code>kernel/src/mem/mod.rs</code>中对内存管理初始化进行扩展，完成页帧到虚拟地址的映射：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>mod page_table;

/// 内存页大小
pub const PAGE_SIZE: usize = 4096;

/// 堆内存页数量
pub const HEAP_PAGES: usize = 1024;

/// 堆内存基地至
pub const HEAP_BASE: usize = 0xFFFF_FF00_0000_0000;

/// 初始化内存管理
pub fn init&lt;'a&gt;(memory_regions: &amp;'a mut MemoryRegions) {
    // 1. 创建物理页帧分配器，并分配一段连续物理页帧作为内核堆内存
    [...]
    let heap_frame = allocate_frame_cont(HEAP_PAGES).expect(&quot;Failed to allocate frame&quot;);
    // 2. 将堆内存的每个页帧映射到相应虚拟地址
    let page_table = PageTable::new(kernel_base());
    let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE;
    for i in 0..HEAP_PAGES {
        let vaddr = HEAP_BASE + i * PAGE_SIZE;
        let paddr = heap_frame + i * PAGE_SIZE;
        page_table.map(vaddr, paddr, flags);
    }
}

/// 获取内核态虚存基地址
pub fn kernel_base() -&gt; usize {
    Cr3::read().0.start_address().as_u64() as _
}
<span class="boring">}</span></code></pre></pre>
<blockquote>
<p><em><strong>练习2</strong></em></p>
<p>新分配一段连续的物理页，并映射到一段连续的虚存空间中。</p>
</blockquote>
<h3 id="实现堆内存分配器选做"><a class="header" href="#实现堆内存分配器选做">实现堆内存分配器（*选做）</a></h3>
<p>Rust中为使用堆内存需要一个实现了<code>GlobalAlloc trait</code>的堆内存分配器，在可以使用<code>std</code>标准库的场景下，标准库实现了堆内存分配器，我们就可以直接使用堆。但在内核开发中没有<code>std</code>库，需要自己实现堆内存分配器。</p>
<blockquote>
<p><em><strong>练习3</strong></em></p>
<p>将新映射的虚存空间作为内核的堆内存，以支持动态数据结构。</p>
<p>提示</p>
<ul>
<li>可以参照<a href="https://doc.rust-lang.org/std/alloc/index.html">官方文档</a>中的示例自己实现一个堆内存分配器，也可以直接使用社区中已有的第三方库<a href="https://crates.io/crates/buddy_system_allocator">buddy_system_allocator</a>实现堆内存分配器。之后需要取消kernel/src/lib.rs文件头部alloc相关代码的注释。</li>
<li>完成后，我们可以使用诸如Vec、String等使用堆内存的数据结构，可设计测试用例验证分配器正确性。</li>
</ul>
</blockquote>
<h3 id="命令解释器的重新实现选做"><a class="header" href="#命令解释器的重新实现选做">命令解释器的重新实现（*选做）</a></h3>
<p>大家在实验2中的命令解释器实现是如果使用了<code>String</code>类型，会因为没有实现Rust中的<code>GlobalAlloc trait</code>而报错，因此，在练习3的基础上，我们现在可以用更加简洁优美的方式重新实现命令解释器。</p>
<blockquote>
<p>*** 练习4***</p>
<p>请使用<code>String</code>类型重新实现命令解释器的相关功能。</p>
</blockquote>
<h2 id="小结-2"><a class="header" href="#小结-2">小结</a></h2>
<p>请在完成实验之后提交实验报告和所有源码，实验报告应记录实验过程和主要画面。</p>
<p>通过本实验，相信大家能够对内存管理有更加深入的理解。</p>
<h2 id="下一个实验-2"><a class="header" href="#下一个实验-2">下一个实验</a></h2>
<p>实现系统对协程与异步的支持。请提前学习Rust语言特性中与协程有关的相关内容并复习进程管理的相关知识点。</p>
<h2 id="参考资料-3"><a class="header" href="#参考资料-3">参考资料</a></h2>
<ol>
<li>Rust官方文档示例：<a href="https://doc.rust-lang.org/std/alloc/index.html">https://doc.rust-lang.org/std/alloc/index.html</a></li>
<li>buddy_system_allocator库：<a href="https://crates.io/crates/buddy_system_allocator">https://crates.io/crates/buddy_system_allocator</a></li>
</ol>
<div style="break-before: page; page-break-before: always;"></div><h1 id="实验4"><a class="header" href="#实验4">实验4：</a></h1>
<ol>
<li>
<p>掌握协程执行的方法原理，掌握实现键盘事件流协程的方法。参考<a href="https://os.phil-opp.com/">blog_os</a>教程的<a href="https://os.phil-opp.com/async-await">Async/Await</a>章节，理解<code>kernel/src/task/executor.rs</code>和<code>kernel/src/task/keyboard.rs</code>两个文件中的代码。</p>
</li>
<li>
<p>模仿实现鼠标事件流协程。仿照<code>kernel/src/task/keyboard.rs</code>文件中实现键盘事件流协程的方法，新建源码文件<code>kernel/src/task/mouse.rs</code>，并实现鼠标事件流协程。</p>
</li>
</ol>
<blockquote>
<p>提示</p>
<ul>
<li>仿照<code>print_keypresses</code>实现<code>print_mousemovements</code>异步函数，负责执行<code>kernel/src/driver/idt.rs</code>文件中针对鼠标中断的处理逻辑。</li>
<li>仿照<code>kernel/src/main.rs</code>文件中添加<code>print_keypresses</code>任务的方法，添加<code>print_mousemovements</code>任务。</li>
</ul>
</blockquote>
<ol start="3">
<li>编写协程。实现一个sleep函数，可在给定数量的时钟中断后输出一个字符串，等待期间不占用CPU，鼠标可以正常使用。</li>
</ol>
<blockquote>
<p>提示</p>
<ul>
<li>定义<code>AtomicUsize</code>类型全局变量<code>TIMER_TICK</code>记录时钟数，和<code>AtomicWaker</code>类型全局变量<code>TIMER_WAKER</code>唤醒器。在每次时钟中断发生时改变<code>TIMER_TICK</code>，达到预期时间后调用<code>TIMER_WAKER.wake()</code>。</li>
<li>编写<code>SleepFuture</code>，实现<code>Future trait</code>，其中<code>poll</code>函数中<code>TIMER_TICK</code>达到预期时返回<code>Ready</code>，否则注册唤醒器<code>TIMER_WAKER.register(&amp;cx.waker());</code>后返回<code>Pending</code>。</li>
<li>定义异步函数<code>sleep</code>，接收参数<code>睡眠的时钟周期数</code>，函数中创建<code>SleepFuture</code>对象<code>sleep_future</code>，之后执行<code>sleep_future.await</code>，等待结束后打印任意信息。</li>
</ul>
</blockquote>
<ol start="4">
<li>实现一个简单的shell，可以处理来自键盘输入的字符，支持以下命令：</li>
</ol>
<ul>
<li><code>clear</code>: 清屏</li>
<li><code>echo hi</code>: 打印字符串hi</li>
<li><code>export ENV=value</code>: 设置环境变量</li>
<li><code>env</code>: 打印全部环境变量</li>
<li><code>sleep 20 echo hi</code>: 睡眠20个时钟周期后打印hi
<ul>
<li>睡眠支持两种模式，根据环境变量使用相应的模式：
<ul>
<li>同步模式（默认使用）：死循环等待给定时钟周期后打印</li>
<li>异步模式（设置环境变量<code>MODE=async</code>后使用）：异步睡眠给定时钟周期后打印</li>
</ul>
</li>
</ul>
</li>
</ul>

                    </main>

                    <nav class="nav-wrapper" aria-label="Page navigation">
                        <!-- Mobile navigation buttons -->


                        <div style="clear: both"></div>
                    </nav>
                </div>
            </div>

            <nav class="nav-wide-wrapper" aria-label="Page navigation">

            </nav>

        </div>

        <!-- Livereload script (if served using the cli tool) -->
        <script>
            const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
            const wsAddress = wsProtocol + "//" + location.host + "/" + "__livereload";
            const socket = new WebSocket(wsAddress);
            socket.onmessage = function (event) {
                if (event.data === "reload") {
                    socket.close();
                    location.reload();
                }
            };

            window.onbeforeunload = function() {
                socket.close();
            }
        </script>



        <script>
            window.playground_copyable = true;
        </script>


        <script src="elasticlunr.min.js"></script>
        <script src="mark.min.js"></script>
        <script src="searcher.js"></script>

        <script src="clipboard.min.js"></script>
        <script src="highlight.js"></script>
        <script src="book.js"></script>

        <!-- Custom JS scripts -->

        <script>
        window.addEventListener('load', function() {
            window.setTimeout(window.print, 100);
        });
        </script>

    </div>
    </body>
</html>
